#include "simodo/lsp-server/DocumentContext.h"
#include "simodo/lsp-server/ServerContext.h"
#include "simodo/lsp-server/ReportCombiner.h"

#include "simodo/inout/convert/functions.h"
#include "simodo/lsp-client/CompletionItemKind.h"

#include <iostream>
#include <filesystem>
#include <future>
#include <algorithm>

namespace simodo::lsp
{

DocumentContext::DocumentContext(ServerContext & server, 
                                 const std::string & languageId,
                                 const variable::Object & textDocument_object)
    : _server(server)
    , _op(server.document_operation_factory().create(*this,languageId))
    , _valid(false)
{
    open(textDocument_object);
}

bool DocumentContext::open(const variable::Object & textDocument_object)
{
    _valid = false;

    const variable::Value & uri_value = textDocument_object.find(u"uri");
    if (uri_value.type() != variable::ValueType::String)
        return false;

    const variable::Value & languageId_value = textDocument_object.find(u"languageId");
    if (languageId_value.type() != variable::ValueType::String)
        return false;

    const variable::Value & version_value = textDocument_object.find(u"version");
    if (version_value.type() != variable::ValueType::Int)
        return false;

    const variable::Value & text_value = textDocument_object.find(u"text");
    if (text_value.type() != variable::ValueType::String)
        return false;

    std::u16string text = inout::decodeSpecialChars(text_value.getString());

    std::unique_lock<std::shared_timed_mutex> write_locker(_analyze_data_mutex);

    _uri = inout::toU8(uri_value.getString());
    _languageId = inout::toU8(languageId_value.getString());
    _text = text;
    _version = version_value.getInt();
    _file_name = convertUriToPath(_uri);

    return _valid = _is_opened = true;
}

bool DocumentContext::change(const variable::Object & doc_params)
{
    if (!_is_opened) {
        _server.log().error("DocumentContext::change: Document did not opened");
        return false;
    }

    const variable::Value & textDocument_value = doc_params.find(u"textDocument");
    if (textDocument_value.type() != variable::ValueType::Object) {
        _server.log().error("DocumentContext::change: Wrong params structure #1");
        return false;
    }

    std::shared_ptr<variable::Object> textDocument_object = textDocument_value.getObject();
    const variable::Value & version_value = textDocument_object->find(u"version");
    if (version_value.type() != variable::ValueType::Int) {
        _server.log().error("DocumentContext::change: Wrong params structure #2");
        return false;
    }

    /// @note Заготовка для распределённых вычислений + вероятность асинхронности при работе 
    /// с пулом потоков. Не обращаем внимания на запоздавшие старые версии изменений.
    if (version_value.getInt() <= _version) {
        _server.log().warning("DocumentContext::change: Document versions don't match");
        return false;
    }

    const variable::Value & contentChanges_value = doc_params.find(u"contentChanges");
    if (contentChanges_value.type() != variable::ValueType::Array) {
        _server.log().error("DocumentContext::change: Wrong params structure #3");
        return false;
    }

    std::shared_ptr<variable::Array> contentChanges = contentChanges_value.getArray();
    if (contentChanges->values().size() != 1 
     || contentChanges->values()[0].type() != variable::ValueType::Object) {
        _server.log().error("DocumentContext::change: Wrong params structure #4");
        return false;
    }

    std::shared_ptr<variable::Object> contentChangesEvent = contentChanges->values()[0].getObject();
    const variable::Value & text_value = contentChangesEvent->find(u"text");
    if (text_value.type() != variable::ValueType::String) {
        _server.log().error("DocumentContext::change: Wrong params structure #5");
        return false;
    }

    std::u16string text = inout::decodeSpecialChars(text_value.getString());

    std::unique_lock<std::shared_timed_mutex> write_locker(_analyze_data_mutex);

    _text = text;
    _version = version_value.getInt();

    return true;
}

bool DocumentContext::close()
{
    std::unique_lock<std::shared_timed_mutex> write_locker(_analyze_data_mutex);

    if (!_is_opened)
        return false;
    _is_opened = false;

    return true;
}

bool DocumentContext::analyze()
{
    std::unique_lock<std::shared_timed_mutex> write_locker(_analyze_data_mutex);

    if (!_valid)
        return false;
        
    ReportCombiner m;

    _op->analyze(_text, m);

    _server.sending().push(
        variable::JsonRpc {u"textDocument/publishDiagnostics", makeDiagnosticParams(m.messages())} );

    return true;
}

void DocumentContext::copyContent(std::u16string & content) const
{
    std::shared_lock<std::shared_timed_mutex> read_locker(_analyze_data_mutex);
    content = _text;
}

bool DocumentContext::checkDependency(const std::string & uri) 
{
    std::shared_lock<std::shared_timed_mutex> read_locker(_analyze_data_mutex);

    return _op->checkDependency(uri);
}

variable::Value DocumentContext::produceHoverResponse(const simodo::lsp::Position & pos)
{
    std::shared_lock<std::shared_timed_mutex> read_locker(_analyze_data_mutex);

    return _op->produceHoverResponse(pos);
}

variable::Value DocumentContext::produceGotoDeclarationResponse(const simodo::lsp::Position & pos)
{
    std::shared_lock<std::shared_timed_mutex> read_locker(_analyze_data_mutex);

    return _op->produceGotoDeclarationResponse(pos);
}

variable::Value DocumentContext::produceGotoDefinitionResponse(const simodo::lsp::Position & pos)
{
    std::shared_lock<std::shared_timed_mutex> read_locker(_analyze_data_mutex);

    return _op->produceGotoDefinitionResponse(pos);
}

variable::Value DocumentContext::produceCompletionResponse(const simodo::lsp::CompletionParams & completionParams)
{
    std::shared_lock<std::shared_timed_mutex> read_locker(_analyze_data_mutex);

    return _op->produceCompletionResponse(completionParams);
}

variable::Value DocumentContext::produceSemanticTokensResponse()
{
    std::shared_lock<std::shared_timed_mutex> read_locker(_analyze_data_mutex);

    return _op->produceSemanticTokensResponse();
}

variable::Value DocumentContext::produceDocumentSymbolsResponse()
{
    std::shared_lock<std::shared_timed_mutex> read_locker(_analyze_data_mutex);

    return _op->produceDocumentSymbolsResponse();
}

variable::Value DocumentContext::produceSimodoCommandResponse(const std::u16string & command_name)
{
    std::shared_lock<std::shared_timed_mutex> read_locker(_analyze_data_mutex);

    if (!_valid)
        return false;

    return _op->produceSimodoCommandResponse(command_name, _text);
}

////////////////////////////////////////////////////////////////////////////////////
// privates:

variable::Value DocumentContext::makeDiagnosticParams(const std::vector<MessageFullContent> & message_set) const
{
    std::vector<variable::Value> diagnostics;
    // std::u16string file_name_u16 = inout::toU16(_file_name);

    int i = 0;
    for(const MessageFullContent & m : message_set) {
        if (m.location.uri() == _file_name) {
            if (m.level >= inout::SeverityLevel::Error &&  i++ == 50) {
                /// @todo locker?
                _server.log().warning("Too many errors produced by analyze for '" + _uri + "'");
                break;
            }
            diagnostics.push_back(variable::Object {{
                                {u"range",      makeRange(m.location)},
                                {u"severity",   makeSeverity(m.level)},
                                {u"source",     u"SIMODO/loom"},
                                {u"message",    makeMessage(m)},
                            }});
        }
    }

    if (diagnostics.empty() && !message_set.empty()) {
        std::set<std::string> processed_files;
        /// \note Это значит, что возможно были ошибки в других файлах, нужно об этом сказать!
        for(const MessageFullContent & m : message_set) 
            if (m.level >= inout::SeverityLevel::Error && processed_files.find(m.location.uri()) == processed_files.end()) {
                diagnostics.push_back(variable::Object {{
                                    /// \todo У нас нет информации о месте использования файла. 
                                    {u"range",      makeRange({0,0},{1,0})},
                                    {u"severity",   makeSeverity(m.level)},
                                    {u"source",     u"SIMODO/loom"},
                                    {u"message",    u"Problems in the '" + inout::toU16(m.location.uri()) + u"' file: " + makeMessage(m)},
                                }});
                processed_files.insert(m.location.uri());
            }
    }

    std::shared_ptr<variable::Object> res = std::make_shared<variable::Object>();

    /// @todo _parser?
    res->variables().push_back({u"uri",         inout::toU16(_uri)});
    /// @todo _parser?
    res->variables().push_back({u"version",     _version});
    res->variables().push_back({u"diagnostics", variable::Array {diagnostics}});

    return res;
}

std::shared_ptr<variable::Object> 
    DocumentContext::makeRange(const inout::Location & loc)
{
    return makeRange({loc.range().start().line(),loc.range().start().character()},
                     {loc.range().end().line(),loc.range().end().character()});
}

std::shared_ptr<variable::Object> 
    DocumentContext::makeRange(std::pair<int64_t, int64_t> start, std::pair<int64_t, int64_t> end)
{
    std::shared_ptr<variable::Object> res = std::make_shared<variable::Object>();

    res->variables().push_back({u"start", makePosition(start.first,start.second)});
    res->variables().push_back({u"end", makePosition(end.first, end.second)});

    return res;
}

std::shared_ptr<variable::Object> 
    DocumentContext::makeRange(const inout::Range & range)
{
    std::shared_ptr<variable::Object> res = std::make_shared<variable::Object>();

    res->variables().push_back({u"start", makePosition(range.start().line(), range.start().character())});
    res->variables().push_back({u"end", makePosition(range.end().line(), range.end().character())});

    return res;
}

std::shared_ptr<simodo::variable::Object> 
    DocumentContext::makePosition(int64_t line, int64_t character)
{
    std::shared_ptr<variable::Object> res = std::make_shared<variable::Object>();

    res->variables().push_back({u"line", line});
    res->variables().push_back({u"character", character});

    return res;
}                            

int64_t DocumentContext::makeSeverity(inout::SeverityLevel level)
{
    int64_t res;
    switch(level)
    {
    case inout::SeverityLevel::Fatal:
    case inout::SeverityLevel::Error:
        res = 1; // Error 
        break;
    case inout::SeverityLevel::Warning:
        res = 2; // Warning 
        break;
    default:   
        res = 3; // Information 
        break;
    }
    return res;
}

std::u16string DocumentContext::makeMessage(const MessageFullContent & message)
{
    std::u16string res {inout::toU16(message.briefly)};

    if (!message.atlarge.empty())
        res += u'\n' + inout::toU16(message.atlarge);

    return inout::encodeSpecialChars(res);
}

std::string DocumentContext::convertUriToPath(const std::string & uri)
{
    return uri;
}

}